feat(core): Phase 1 — iOS element regions via maestro hierarchy (cross-platform parity, behind env switch)#2202
Draft
Sriram567 wants to merge 4 commits intofeat/maestro-multipart-uploadfrom
Conversation
The Android view-hierarchy resolver is becoming the cross-platform Maestro resolver (per percy-maestro/docs/plans/2026-04-27-001-feat-ios-element-regions-maestro-hierarchy-plan.md Unit 1). Rename + shim is purely additive — no behavior change. - Move src/adb-hierarchy.js → src/maestro-hierarchy.js (git mv preserves history). - Move test/unit/adb-hierarchy.test.js → test/unit/maestro-hierarchy.test.js. - Move test/fixtures/adb-hierarchy/ → test/fixtures/maestro-hierarchy/. - Replace src/adb-hierarchy.js with a 5-line re-export shim. Removed in V1.1 per the plan's deprecation guidance. - Update api.js import to ./maestro-hierarchy.js. - Update logger namespace from core:adb-hierarchy → core:maestro-hierarchy. - Update file header to reflect cross-platform intent (the file body has been maestro-first for some time; the previous file name was always misleading). - Update test describe block + import + fixture path. Behavior unchanged. Subsequent units in Phase 1 will add the iOS branch and api.js dispatch logic; this commit is just the rename so the diffs in those units stay focused.
…parity
Phase 1 Unit 2a per percy-maestro/docs/plans/2026-04-27-001-feat-ios-element-regions-maestro-hierarchy-plan.md.
Lands the platform-dispatch scaffolding and the cross-platform selector
vocabulary alias. Real iOS resolver implementation deferred to Unit 2b
post Phase 0.5 fixture capture (FIXME-PHASE-0.5 in code).
Platform dispatch:
- dump({ platform }) accepts 'android' (default — backwards compatible) or
'ios'. iOS branch reads PERCY_IOS_DEVICE_UDID + PERCY_IOS_DRIVER_HOST_PORT
from env (realmobile-injected per Unit 10a; the wda_port + 2700 formula
is realmobile-owned per maestro_session.rb:831). Warn-skip with
reason='env-missing' if either var is unset. Otherwise calls
runMaestroIosDump which currently returns
{ kind: 'unavailable', reason: 'not-implemented' } as the FIXME-PHASE-0.5
stub.
- iOS path never invokes adb (verified by test).
R1 vocabulary parity (Android `id` alias):
- flattenMaestroNodes (Android branch) now surfaces resource-id under both
`resource-id` AND `id` canonical keys on each node. Customer selectors
`{element: {id: "submit-btn"}}` and `{element: {resource-id: "submit-btn"}}`
resolve the same node. iOS users writing `{id: ...}` and Android users
writing the same yaml hit the same code path. Full unified-key migration
(deprecating `resource-id`) deferred to V1.1.
- SELECTOR_KEYS_UNION = [resource-id, text, content-desc, class, id]
drives firstMatch validation. ANDROID_SELECTOR_KEYS_WHITELIST and
IOS_SELECTOR_KEYS_WHITELIST exported separately for callers that want
per-platform validation.
Tests added:
- Android `id` alias resolves same bbox as `resource-id` (3 tests).
- iOS env-missing path (3 tests covering each env-var combination).
- iOS env-set returns 'not-implemented' (FIXME stub).
- iOS dispatch never invokes adb.
- Default (no platform arg) preserves Android behavior.
Smoke-tested via direct node import; full @percy/core test suite has 27
pre-existing failures in Unit / Install in executable Chromium (unrelated
infrastructure issue), but no regressions in the resolver tests.
Phase 1 Unit 3 per percy-maestro/docs/plans/2026-04-27-001-feat-ios-element-regions-maestro-hierarchy-plan.md.
Wires the maestro-hierarchy resolver into the /percy/maestro-screenshot
relay's iOS element-region dispatch, gated by an env switch so default
(unset) behavior is unchanged. Phase 0.5 empirical probe gates the
default flip to the new path; Phase 4 deletes the legacy iOS branch.
- New: read PERCY_IOS_RESOLVER from process.env. When equal to
'maestro-hierarchy', iOS element regions flow through the same
lazy maestroDump({ platform: 'ios' }) + per-region firstMatch
pattern Android already uses. When unset (or any other value),
legacy WDA-direct path remains active — no behavior change for
customers in production today.
- Refactor: the up-front PNG-parse + resolveIosRegions block now only
fires when the env switch is OFF. With the switch on, that work is
unnecessary (the resolver is engineered to be lazy + per-region).
- The cross-platform branch in the per-region loop now also covers iOS
when the switch is on. Same shape as Android: cachedDump lazy memo,
warn-skip on hierarchy-unavailable, firstMatch + bbox forward on
success.
Today (env switch unset): only the cross-platform Android path is
exercised. The iOS branch with the switch on is exercised by the
maestro-hierarchy unit tests landed in Unit 2a (which covers the
'env-missing' and 'not-implemented' stub paths). Unit 4 adds the
parity test that exercises both platforms via the same handler.
A real production rollout flips the default to 'maestro-hierarchy'
in Phase 4 (Unit 9) after Phase 0.5 PASSes; until then, keep the
default off.
Phase 1 Unit 4 per percy-maestro/docs/plans/2026-04-27-001-feat-ios-element-regions-maestro-hierarchy-plan.md.
New test file: test/unit/maestro-hierarchy.parity.test.js. Locks in the
contract that both platform branches return the same { kind, ... } envelope,
that the public API surface (SELECTOR_KEYS_WHITELIST + per-platform
whitelists) is consistent, and that platform dispatch isolates the env-var
reads (Android never reads PERCY_IOS_*; iOS never reads ANDROID_SERIAL).
Bug fix discovered during smoke test:
- flattenNodes (the XML/uiautomator code path) was missing the R1 `id`
alias surface that flattenMaestroNodes (the maestro CLI JSON path)
already had. So `firstMatch(nodes, { id: 'X' })` worked when nodes came
from the maestro path but returned null when nodes came from the adb
fallback path. Now both code paths surface resource-id under both
`resource-id` and `id` keys consistently.
iOS-side parity assertions in this test are scoped to what Unit 2a's stub
can actually cover — envelope shape, whitelist exports, dispatch isolation.
The Phase 4 follow-up (post Phase 0.5 + Unit 2b) extends this file with
real iOS attribute-mapping assertions backed by a captured iOS hierarchy
fixture.
Smoke-tested via direct node import. The full @percy/core test suite has
27 pre-existing Chromium-installer failures unrelated to this work.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 1 of the strategic pivot from WDA-direct (#2201) to a cross-platform
maestro hierarchyresolver. Default behavior unchanged — the new pathis gated behind a
PERCY_IOS_RESOLVERenv switch that defaults towda-direct. Customers see no production change until Phase 0.5 empiricalprobe passes and Phase 4 flips the default.
Reference plan:
percy-maestro/docs/plans/2026-04-27-001-feat-ios-element-regions-maestro-hierarchy-plan.md(local-only repo; not pushed to GitHub yet).
Why pivot
The 2026-04-22 brainstorm rejected
maestro hierarchyfor iOS as"session-exclusive" without empirical testing. The 2026-04-27 spike (run
on BS iOS host 52) proved otherwise:
/nix/storeon iOS BS hosts (multipleversions, all wrapping Maestro 1.39.10). Java 17 (Zulu) is available.
maestro --device=<udid> --driver-host-port <P>with
P = wda_port + 2700deterministically (seerealmobile/maestro_session.rb:831). Same gRPC-over-iproxy transport anexternal
maestro hierarchyinvocation would use.dev.mobile.maestro-driver-iosUITests.xctrunner-{2.1,2.2,3,4}.appisprecached on the host, structurally analogous to Android's
dev.mobile.maestroapp.The brainstorm's "session-exclusive" claim conflated
maestro hierarchywith
maestro studio. Studio is exclusive; hierarchy on Androiddemonstrably is not (production usage in
maestro-hierarchy.jsforyears).
Customer benefit: one resolver module across both platforms; same yaml
shape; same docs; one mental model. Engineering benefit: drops the
realmobile
wda-meta.jsoncontract (~250 lines of cross-tenantfilesystem hardening + 8 security acceptance tests + cross-team
coordination) in favor of two env vars realmobile already knows.
What lands here (Phase 1, gated by env switch)
4 commits, in order:
9e2f3815Unit 1 — Renameadb-hierarchy.js→maestro-hierarchy.js403d89fcUnit 2a — Platform-dispatch scaffolding indump({ platform }).iOS branch reads
PERCY_IOS_DEVICE_UDID+PERCY_IOS_DRIVER_HOST_PORTenv vars (realmobile-injected; the
wda_port + 2700formula stays inrealmobile) and currently returns
{ kind: 'unavailable', reason: 'not-implemented' }as aFIXME-PHASE-0.5stub. The real iOSattribute-mapping logic lands in Unit 2b (separate PR) post Phase 0.5.
Also lands R1 vocabulary parity: Android's
flattenNodesandflattenMaestroNodesnow surfaceresource-idvalue under bothresource-idANDidcanonical keys. Customers writing{element: {id: "X"}}work on Android too.1b98ece6Unit 3 —api.jsdispatch behindPERCY_IOS_RESOLVERenv switch. Default off → existing WDA-direct iOS path. Switch on →
cross-platform Android-style lazy
dump+ per-regionfirstMatch.616cdd56Unit 4 — Cross-platform parity test(
maestro-hierarchy.parity.test.js) + bug fix for the XML-pathidalias (smoke-test caught a divergence between the JSON path and XML
path).
Phase status
default. ✅ Done.
session. BLOCKED on a current BS Maestro infra outage (5 builds in
a row failed at the
MAESTRO test startedstep with no further detail— see
percy-maestro/docs/experiments/2026-04-27-maestro-hierarchy-spike/findings.md).on Unit 10a in realmobile (env-var exports + denylist filter for
cross-tenant override defense).
Decision tree if/when Phase 0.5 runs
A1.5 (probe interleaved with
tapOn/scrollfrom parent flow bothsucceed) + A3 (
attributes.identifier+attributes.elementTypeinteger present in JSON) all PASS, A2 (latency p95 < 3s) measured,
A4 (end-to-end flow latency < 30% of session timeout on a 10-screenshot
flow) within target → flip the env switch default to
maestro-hierarchy; merge Phase 4 PR; delete feat(core): iOS element regions via WDA-direct (Plan A — gated for deletion in Phase 4) #2201 modules.PER-7281 with realmobile.
Testing
Smoke-tested via direct node import — all whitelist exports correct,
iOS env-missing path returns
'env-missing', env-set returns'not-implemented'stub, R1 alias parity holds through both XML(uiautomator) and JSON (maestro CLI) code paths. Full
@percy/coretestsuite has 27 pre-existing Chromium-installer failures unrelated to this
PR.
New tests:
test/unit/maestro-hierarchy.test.js— extends existing Android testswith R1
idalias coverage (3 tests) and iOS branch coverage (5 tests).test/unit/maestro-hierarchy.parity.test.js— new file. Cross-platformenvelope shape + dispatch isolation + whitelist surface.
Post-Deploy Monitoring & Validation
No additional operational monitoring required: Phase 1 ships with PERCY_IOS_RESOLVER env switch off by default; no production code path changes for any customer until the default is flipped in Phase 4 (separate PR, gated by Phase 0.5 PASS).Phase 4's PR will carry theoperational validation plan.
🤖 Generated with Claude Opus 4.7 (1M context, extended thinking) via Claude Code